/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.veditor.model;

import offset.nodes.client.veditor.model.DataModel;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Stack;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.html.HTML;
import offset.nodes.Constants;
import offset.nodes.client.editor.model.ComponentModel;
import offset.nodes.client.editor.model.PrintHandler;
import offset.nodes.client.model.HttpUtils;
import offset.nodes.client.model.StringArrayConverter;
import offset.nodes.client.model.XmlCharacterFilter;
import offset.nodes.client.virtual.model.SimpleQuery;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

public class DataWriter {

    Stack path = new Stack();
    WriterElement root = null;
    LinkedList<Element> secondaryQueryRoots = new LinkedList<Element>();
    boolean writePrimaryQuery = true;
    DataModel model;

    public DataWriter(DataModel model) {
        this.model = model;
    }

    class WriterElement {

        String name;
        AttributesImpl attributes = new AttributesImpl();
        HashMap<String, Integer> attributeMap = new HashMap<String, Integer>();
        String content;
        LinkedList children = new LinkedList();

        public WriterElement(String name) {
            this.name = name;
        }

        public WriterElement(String name, String content) {
            this.name = name;
            this.content = content;
        }

        public AttributesImpl getAttributes() {
            return attributes;
        }

        public void setAttributes(AttributesImpl attributes) {
            this.attributes = attributes;
        }

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public LinkedList getChildren() {
            return children;
        }

        public void setChildren(LinkedList children) {
            this.children = children;
        }

        public void addAttribute(String name, String value) {
            if (value == null)
                return;
            
            name = getLastPathComponent(name);
            if (name.startsWith("@"))
                name = name.substring(1);
            if (!attributeMap.containsKey(name)) {
                // rename recurring attributes as name, name1, name2 ...
                attributeMap.put(name, new Integer(attributes.getLength()));
                attributes.addAttribute("", "", name, "CDATA", value);
            }
            else {
                Integer index = attributeMap.get(name);
                StringArrayConverter converter = new StringArrayConverter(attributes.getValue(index.intValue()));
                converter.addString(value);
                attributes.setValue(index.intValue(), converter.toString());
            }
        }

        public void addChild(WriterElement e) {
            children.add(e);
        }

        public void removeChild(WriterElement e) {
            children.remove(e);
        }
    }

    protected void reset() {
        Stack path = new Stack();
        WriterElement root = null;
    }

    protected String getLastPathComponent(String name) {
        int slash = name.lastIndexOf("/");
        if (slash >= 0)
            name = name.substring(slash + 1);
        return name;
    }

    protected String getFirstPathComponent(String name) {
        int slash = name.indexOf("/"); // do not count the root itself
        if (slash >= 0)
            name = name.substring(0, slash);
        return name;
    }

    protected boolean isAttribute(String name) {
        return getLastPathComponent(name).startsWith("@");
    }

    protected String getContent(Element e) throws SAXException, BadLocationException {
        if (e.getName().equals(HTML.Tag.SPAN.toString())) {
            int childIndex = model.getChildIndex(e);
            Element data = e.getParentElement().getElement(childIndex + 1);
            String content = model.getDocument().getText(data.getStartOffset(), data.getEndOffset() - data.getStartOffset());
            return XmlCharacterFilter.filterCharacters(content.toCharArray(), 0, content.length());
        }
        for (int i = 0; i < e.getElementCount(); i++) {
            Element child = e.getElement(i);
            if (!child.getName().equals(HTML.Tag.CONTENT.toString()) && !child.getName().equals(HTML.Tag.IMPLIED.toString()))
                return null;
        }
        String content = null;
        if (e.getName().equals(HTML.Tag.A.toString())) {
            content = model.getDocument().getText(e.getStartOffset(), e.getEndOffset() - e.getStartOffset());
            if (content.endsWith(" "))
                content = content.substring(0, content.length() - 1);
        }
        else if (e.getElementCount() >= 1 && model.isHyperlink(e.getElement(0))) {
            Element hyperlink;
            if (model.isArtificialElement(e.getElement(0)))
                hyperlink = model.getHtmlElement(e.getElement(0)).getElement(0);
            else 
                hyperlink = model.getHtmlElement(e).getElement(0);
            content = (String) hyperlink.getAttributes().getAttribute(HTML.Attribute.HREF);
            if (content.indexOf(Constants.PAR_NODE_REFERENCE) >= 0) {
                content = HttpUtils.getParameters(content).get(Constants.PAR_NODE_REFERENCE);
            }
        } else
            content = model.getDocument().getText(e.getStartOffset(), e.getEndOffset() - e.getStartOffset() - 1);
        
        return XmlCharacterFilter.filterCharacters(content.toCharArray(), 0, content.length());
    }

    protected void extractData(Element e) throws Exception {
        boolean elementPushed = false;
        HTML.Tag tag = model.getInlineElementTag(e);
        if (tag != null)
            e = model.new InlineElement(e, tag, (AttributeSet) e.getAttributes().getAttribute(tag));
        if (model.isData(e) && !model.readOnly(e)) {
            String name = model.discardFilters(model.getData(e));

            if (writePrimaryQuery && name.startsWith(model.SECONDARY_QUERY)) {
                if (e.getAttributes().isDefined(model.ATTRIBUTE_NAME_PRIMARY_REFERENCE)) {
                    // this is a readonly secondary query
                    // set the reference property
                    String referenceValue = (String) e.getAttributes().getAttribute(model.ATTRIBUTE_NAME_PRIMARY_REFERENCE);
                    int end = name.indexOf("/", model.SECONDARY_QUERY.length());
                    if (end >= 0)
                        name = name.substring(1, end);
                    SimpleQuery query = model.secondaryQueries.getQuery(name);
                    String referenceName = query.getReferenceProperty();
                    WriterElement we = (WriterElement) path.peek();
                    we.addAttribute(referenceName, referenceValue);
                    // return without adding it to the secondaryQueryRoots, as it is readonly
                }
                else
                    secondaryQueryRoots.add(e);
                return;
            }

            if (isAttribute(name) && path.size() > 0) {
                /* Discard binary elements once they are identified,
                 * i.e. as soon as the binary attribute has been found
                 */
                if (e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_BINARY)) {
                    for (int i = path.size() - 1; i >= 0; i--) {
                        WriterElement we = (WriterElement) path.get(i);
                        if (we.getAttributes().getValue(Constants.JCR_UUID) != null) {
                            WriterElement parent = (WriterElement) path.get(i - 1);
                            parent.removeChild(we);

                            return;
                        }

                    }
                }

                WriterElement we = (WriterElement) path.peek();
                we.addAttribute(name, getContent(e));
            }
            else {
                if (e.getAttributes().isDefined(ComponentModel.ATT_COMPONENT_ID))
                    return;
                
                WriterElement newElement = new WriterElement(name, getContent(e));
                if (path.size() > 0) {
                    WriterElement parent = (WriterElement) path.peek();
                    parent.addChild(newElement);
                }
                else {
                    root = newElement;
                    String type = model.getSchemaName(e);
                    root.addAttribute(Constants.JCR_PRIMARY_TYPE, type);
                }
                String id = model.getId(e);
                if (id != null)
                    newElement.addAttribute(Constants.JCR_UUID, id);
                path.push(newElement);
                elementPushed = true;
            }
        }
        if (model.readOnly(e))
            return;
        for (int i = 0; i < e.getElementCount(); i++) {
            extractData(e.getElement(i));
        }
        if (elementPushed)
            path.pop();
    }

    protected void writeData(WriterElement e, ContentHandler out) throws SAXException {
        if (e == null)
            return;
        String name = getLastPathComponent(e.getName());
        out.startElement("", "", name, e.getAttributes());
        if (e.getContent() != null && e.getContent().length() > 0)
            out.characters(e.getContent().toCharArray(), 0, e.getContent().length());
        Iterator i = e.getChildren().iterator();
        while (i.hasNext()) {
            writeData((WriterElement) i.next(), out);
        }
        out.endElement("", "", name);
    }

    public void write(Element dataRoot, OutputStream out) throws Exception {
        ContentHandler handler = new PrintHandler(out);
        handler.startElement("", "", model.ELEMENT_DOCUMENT, null);
        extractData(dataRoot);
        writeData(root, handler);
        writePrimaryQuery = false;
        Iterator<Element> i = secondaryQueryRoots.iterator();
        String secondaryQuery = null;
        while (i.hasNext()) {
            reset();
            extractData(i.next());
            String query = getFirstPathComponent(root.getName().substring(1));
            if (!query.equals(secondaryQuery)) {
                if (secondaryQuery != null)
                    handler.endElement("", "", secondaryQuery);
                secondaryQuery = query;
                handler.startElement("", "", secondaryQuery, null);
            }
            writeData(root, handler);
        }
        if (secondaryQuery != null)
            handler.endElement("", "", secondaryQuery);
        handler.endElement("", "", model.ELEMENT_DOCUMENT);
    }
}
